/*
 *  Copyright (c) 2011-2015 The original author or authors
 *  ------------------------------------------------------
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *       The Eclipse Public License is available at
 *       http://www.eclipse.org/legal/epl-v10.html
 *
 *       The Apache License v2.0 is available at
 *       http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */
package com.taobao.middleware.cli.annotations;


import com.taobao.middleware.cli.CLI;
import com.taobao.middleware.cli.CLIException;
import com.taobao.middleware.cli.CommandLine;
import com.taobao.middleware.cli.TypedArgument;
import com.taobao.middleware.cli.TypedOption;
import com.taobao.middleware.cli.impl.ArgumentComparator;
import com.taobao.middleware.cli.impl.DefaultCLI;
import com.taobao.middleware.cli.impl.ReflectionUtils;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * Class responsible for defining CLI using annotations and injecting values extracted by the parser.
 *
 * @author Clement Escoffier <clement@apache.org>
 */
public class CLIConfigurator {
    /**
     * Creates an instance of the given class, and extracts the metadata from the given class.
     *
     * @param clazz the CLI class
     * @return the defined CLI.
     */
    public static CLI define(Class<?> clazz) {
        return define(clazz, false);
    }

    /**
     * Creates an instance of the given class, and extracts the metadata from the given class.
     *
     * @param clazz the CLI class
     * @param caseSensitive
     * @return the defined CLI.
     */
    public static CLI define(Class<?> clazz, boolean caseSensitive) {
        CLI cli = new DefaultCLI();
        cli.setCaseSensitive(caseSensitive);

        // Class annotations
        final Summary summary = clazz.getAnnotation(Summary.class);
        final Description desc = clazz.getAnnotation(Description.class);
        final Hidden hidden = clazz.getAnnotation(Hidden.class);
        final Name name = clazz.getAnnotation(Name.class);

        if (name == null) {
            throw new IllegalArgumentException("The command cannot be defined, the @Name annotation is missing.");
        }
        if (name.value() == null || name.value().isEmpty()) {
            throw new IllegalArgumentException("The command cannot be defined, the @Name value is empty or null.");
        }
        cli.setName(name.value());

        if (summary != null) {
            cli.setSummary(summary.value());
        }
        if (desc != null) {
            cli.setDescription(desc.value());
        }
        if (hidden != null) {
            cli.setHidden(true);
        }

        // Setter annotations
        final List<Method> methods = ReflectionUtils.getSetterMethods(clazz);
        for (Method method : methods) {
            final Option option = method.getAnnotation(Option.class);
            final Argument argument = method.getAnnotation(Argument.class);

            if (option != null) {
                cli.addOption(createOption(method));
            }
            if (argument != null) {
                cli.addArgument(createArgument(method));
            }
        }

        // Sort the argument by index.
        Collections.sort(cli.getArguments(), new ArgumentComparator());

        return cli;
    }

    @SuppressWarnings("unchecked")
    private static com.taobao.middleware.cli.Option createOption(Method method) {
        TypedOption opt = new TypedOption();

        // Option
        Option option = method.getAnnotation(Option.class);
        opt.setLongName(option.longName())
                .setShortName(option.shortName())
                .setMultiValued(option.acceptMultipleValues())
                .setSingleValued(option.acceptValue())
                .setArgName(option.argName())
                .setFlag(option.flag())
                .setHelp(option.help())
                .setRequired(option.required());

        // Description
        Description description = method.getAnnotation(Description.class);
        if (description != null) {
            opt.setDescription(description.value());
        }

        Hidden hidden = method.getAnnotation(Hidden.class);
        if (hidden != null) {
            opt.setHidden(true);
        }

        if (ReflectionUtils.isMultiple(method)) {
            opt.setType(ReflectionUtils.getComponentType(method, 0))
                    .setMultiValued(true);
        } else {
            final Class<?> type = method.getParameterTypes()[0];
            opt.setType(type);
            if (type != Boolean.TYPE && type != Boolean.class) {
                // In the case of a boolean, it may be a flag, need explicit settings.
                opt.setSingleValued(true);
            }
        }

        ConvertedBy convertedBy = method.getAnnotation(ConvertedBy.class);
        if (convertedBy != null) {
            opt.setConverter(ReflectionUtils.newInstance(convertedBy.value()));
        }
        ParsedAsList parsedAsList = method.getAnnotation(ParsedAsList.class);
        if (parsedAsList != null) {
            opt.setParsedAsList(true).setListSeparator(parsedAsList.separator());
        }

        // Default value
        DefaultValue defaultValue = method.getAnnotation(DefaultValue.class);
        if (defaultValue != null) {
            opt.setDefaultValue(defaultValue.value());
        }

        opt.ensureValidity();

        return opt;
    }

    @SuppressWarnings("unchecked")
    private static com.taobao.middleware.cli.Argument createArgument(Method method) {
        TypedArgument arg = new TypedArgument();

        // Argument
        Argument argument = method.getAnnotation(Argument.class);
        arg.setIndex(argument.index());
        arg.setArgName(argument.argName());
        arg.setRequired(argument.required());

        // Description
        Description description = method.getAnnotation(Description.class);
        if (description != null) {
            arg.setDescription(description.value());
        }

        if (ReflectionUtils.isMultiple(method)) {
            arg.setType(ReflectionUtils.getComponentType(method, 0))
                    .setMultiValued(true);
        } else {
            Class type = method.getParameterTypes()[0];
            arg.setType(type);
        }

        Hidden hidden = method.getAnnotation(Hidden.class);
        if (hidden != null) {
            arg.setHidden(true);
        }

        ConvertedBy convertedBy = method.getAnnotation(ConvertedBy.class);
        if (convertedBy != null) {
            arg.setConverter(ReflectionUtils.newInstance(convertedBy.value()));
        }

        // Default value
        DefaultValue defaultValue = method.getAnnotation(DefaultValue.class);
        if (defaultValue != null) {
            arg.setDefaultValue(defaultValue.value());
        }

        return arg;
    }

    private static Object getOptionValue(Method method, String name, CommandLine commandLine) {
        final com.taobao.middleware.cli.Option option = commandLine.cli().getOption(name);
        if (option == null) {
            return null;
        }
        boolean multiple = ReflectionUtils.isMultiple(method);
        if (multiple) {
            return createMultiValueContainer(method, commandLine.getOptionValues(name));
        }
        return commandLine.getOptionValue(name);
    }

    private static Object getArgumentValue(Method method, int index, CommandLine commandLine) {
        final com.taobao.middleware.cli.Argument argument = commandLine.cli().getArgument(index);
        if (argument == null) {
            return null;
        }

        boolean multiple = ReflectionUtils.isMultiple(method);
        if (multiple) {
            return createMultiValueContainer(method, commandLine.getArgumentValues(argument.getIndex()));
        }
        return commandLine.getArgumentValue(argument.getIndex());
    }

    /**
     * Injects the value in the annotated setter methods ({@link Option} and {@link Argument}.
     *
     * @param cli    the cli
     * @param object the object to be injected
     * @throws CLIException if an injection issue occurred.
     */
    public static void inject(CommandLine cli, Object object) throws CLIException {
        final List<Method> methods = ReflectionUtils.getSetterMethods(object.getClass());
        for (Method method : methods) {
            Option option = method.getAnnotation(Option.class);
            Argument argument = method.getAnnotation(Argument.class);
            if (option != null) {
                String name = option.longName();
                if (name == null) {
                    name = option.shortName();
                }
                try {
                    Object injected = getOptionValue(method, name, cli);
                    if (injected != null) {
                        method.setAccessible(true);
                        method.invoke(object, injected);
                    }
                } catch (Exception e) {
                    throw new CLIException("Cannot inject value for option '" + name + "'", e);
                }
            }

            if (argument != null) {
                int index = argument.index();
                try {
                    Object injected = getArgumentValue(method, index, cli);
                    if (injected != null) {
                        method.setAccessible(true);
                        method.invoke(object, injected);
                    }
                } catch (Exception e) {
                    throw new CLIException("Cannot inject value for argument '" + index + "'", e);
                }
            }

        }
    }

    private static <T> Object createMultiValueContainer(Method setter, List<T> values) {
        final Class<?> type = setter.getParameterTypes()[0];
        if (type.isArray()) {
            Object array = Array.newInstance(type.getComponentType(), values.size());
            for (int i = 0; i < values.size(); i++) {
                Array.set(array, i, values.get(i));
            }
            return array;
        }

        if (Set.class.isAssignableFrom(type)) {
            return new LinkedHashSet<T>(values);
        }

        if (List.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type)) {
            return values;
        }

        return null;
    }

}
